iT邦幫忙

2024 iThome 鐵人賽

DAY 19
1
AI/ ML & Data

一個Kaggle金牌解法是如何誕生的?跟隨Kaggle NLP競賽高手的討論,探索解題脈絡系列 第 19

[Day 19]Data-Model-Model Training! 淺談如何在有限的資源上高效地訓練LLM

  • 分享至 

  • xImage
  •  

今天會帶大家使用 unsloth 這個好用的 library ,在單張消費級顯卡上微調自己的大語言模型🚀🚀!

前情提要

Day 17我們結合多種不同資料來源與技巧,生成了增強版的訓練數據;Day 18 我們深入研究評測通用型 LLM 的常見 Benchmark 以及 Leaderboard,知道該如何挑選適合我們領域任務的大語言模型後,今天就要開始用我們自己的 Science Exam 資料來 fine-tune 一個答題LLM囉~

image

由於 LLM 動輒幾十億、幾百億的參數,所以微調 LLM 和微調 BERT, Deberta 等模型相比會更複雜一些。我們在開始訓練 LLM 之前,需要先知道兩件事情- Quantization(量化)LoRA & QLoRA 技術。

Quantization 量化

大多數模型本身使用32位浮點數(通常稱為全精度)表示,假設我們現在有一個包含700億參數的模型,這時就需要280GB的VRAM來加載模型。但如果能將所有參數用16位浮點數表示,所需的內存大小就可以直接減少一倍。
image

因此,減少模型參數的精度(不僅在推理時,訓練過程中也是如此)變得非常具有吸引力,也讓我們這些算力貧民有機會入場 LLM 的遊戲。

然而,這種策略並非沒有代價。

隨著使用的浮點位數減少,模型的準確率通常也會隨之降低,因此如何在不損失準確性的前提下縮減位寬,這就是 Quantization 量化技術在研究的事情!

但如同Day1文章規劃中提到,這個系列文會主要專注在Kaggle解題策略的思考脈絡,關於已經大量應用的模型、演算法等的原理就不會多去著墨。網上有非常多相關的文章和影片在介紹 LLM Quantization 的基本原理與不同技術的差異,在下面整理一些我個人覺得非常優秀、受益頗多的參考資料,有興趣的朋友不妨移步這些資源後再回來接續本文呦!

推薦 Quantization 的相關參考資料:

  1. (文章)LLM Note Day 14 - 量化 Quantization
  2. (影片)LLaMa GPTQ 4-Bit Quantization. Billions of Parameters Made Smaller and Smarter. How Does it Work?
  3. (Code Tutorial) How does quantization work?

LoRA & QLoRA

進入 LLM 的時代後,基本上單張消費級顯卡就不可能再像以前訓練BERT 一樣,做 LLM FFT(Full Fine-Tuning) 了。

這時候,我們就需要借助 LoRA 的力量,也就是一種通過凍結預訓練模型的大部分權重,在模型旁邊外掛一個比較小的模型,訓練時 backward 只更新這個小模型的參數,forward 時則把原本的 pretrained weight 和小模型的結果相加起來當作最終結果的一種計算方式。
透過 LoRA 可以節省訓練模型所花的時間,畢竟只需要更新一小部分的參數,但同時,也可以節省 VRAM 需要的空間呦~

image
你可能會懷疑,這哪有節省空間?原本有的 Pretrained weights 還是在,然後現在又加上一個外掛小模型,空間不減反增呀!

別著急,推薦大家可以看看【LLM專欄】All about Lora這篇文章,裡面有詳細解釋 LoRA 的原理,當然也可以解決你這個疑惑。

除此之外我也推薦大家參考下面的參考資料,會對 LoRA 有更深入的了解~

推薦 LoRA 的相關參考資料:

  1. (文章)LLM Note Day 25 - PEFT & LoRA 訓練框架
  2. (Code Tutorial)How to Finetune LLMs with LoRA?
  3. (文章)【LLM專欄】All about Lora
  4. (文章)LoRa、QLoRA 和 QA-LoRA:透過低秩矩陣分解實現大型語言模型的高效適應性

總之,在 fine-tune LLM 時,Quantization 和 LoRA 是兩個相輔相成的技術,不僅可以加快訓練與推理速度,也可以減少 LLM 需要的 VRAM 空間,現在有很多訓練 LLM 的框架都會預設使用者會調用這兩個功能。2023 年有一篇 QLoRA 的論文,直接把 Quantization 和 LoRA 結合以來,發展更高效地訓練 LLM 的技術,有興趣的朋友也可以參考上面推薦資料的第四項。

一起用 Unsloth Fine-Tune LLM

Unsloth 是一個用於加速大模型訓練的開源項目,通過使用 OpenAI 的 Triton 重新 implement 模型計算過程,大幅提升訓練效率並降低VRAM使用,同時保證他們重寫後的模型的計算結果會和原始版本一致,實現中不存在近似計算,模型訓練的精度損失為零。

Unsloth 兼容大多數主流 GPU 設備,如 V100、T4、Titan V、RTX 20、30、40 系列、A100、H100、L40 等,並支持 LoRA 和 QLoRA 的加速訓練與高效顯存管理,同時兼容 Flash Attention 技術。

不過!

現在 Unsloth 目前只支援單卡計算,但這對我們這種沒有實驗室大量硬體運算資源的一般玩家來說,反而不是什麼問題。

一開始我是被他們秀出的成績單吸引:
image
在Colab T4 上跑 OASST Benchmark,速度比🤗抱抱臉原始的 implementation 快了 1.95 倍接近 2 倍,VRAM 減少了 43.3% ,非常有感地降低硬體負荷,而且這些都是在沒有損失預測精度的情況下,簡直就是天上掉餡餅,讓我們吃了一頓免費的午餐😋!

而且更棒棒的是,Unsloth 和 HuggingFace 的生態兼容,所以我們不用為了使用它,重新學一些複雜的 api,大多可以沿用之前開發寫的代碼。

話不多說,我們就帶大家用 Unsloth 和之前做好的 1K Wiki 擴增版訓練數據集,一起來 Fine-Tune 我們的 LLM 吧!

這次我們選用 Meta-Llama-3.1-8B 的 pretrained LLM 來 fine-tuned。(以下code部分參考自2)

  • 首先先安裝必要的 library 並定義一些 parameter:
from unsloth import FastLanguageModel
import torch
import pandas as pd
from datasets import Dataset
max_seq_length = 720 # Choose any! We auto support RoPE Scaling internally!
dtype = None # None for auto detection. Float16 for Tesla T4, V100, Bfloat16 for Ampere+
load_in_4bit = True # Use 4bit quantization to reduce memory usage. Can be False.

load_in_4bits 啟用之後就會調用 4 bits quantization 技術,一方面減少 VRAM 用量,一方面加速運算速度。目前應該只有支援 load_in_4bits 如果改成 load_in_8bits 會報錯。

  • 用 FastLanguageModel Load 模型
    FastLanguageModel 是 unsloth 實作的 model wrapper。
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = "unsloth/Meta-Llama-3.1-8B",
    max_seq_length = max_seq_length,
    dtype = dtype,
    load_in_4bit = load_in_4bit,
    # load_in_8bit = load_in_4bit,
    # token = "hf_...", # use one if using gated models like meta-llama/Llama-2-7b-hf
)

我們可以到 https://huggingface.co/unsloth 這邊去看有哪些支援 4bit pre quantized 的模型。
以前下載 7B, 13B 的模型因為檔案有好幾十GB,下載完真的要等可能半小時以上,但現在選擇這些 pre-quantized 的模型下載會很快,不到五分鐘就結束了。
下面也列出已經 pre-quantized 4 bits 的模型給大家參考:

fourbit_models = [
    "unsloth/Meta-Llama-3.1-8B-bnb-4bit",      # Llama-3.1 15 trillion tokens model 2x faster!
    "unsloth/Meta-Llama-3.1-8B-Instruct-bnb-4bit",
    "unsloth/Meta-Llama-3.1-70B-bnb-4bit",
    "unsloth/Meta-Llama-3.1-405B-bnb-4bit",    # We also uploaded 4bit for 405b!
    "unsloth/Mistral-Nemo-Base-2407-bnb-4bit", # New Mistral 12b 2x faster!
    "unsloth/Mistral-Nemo-Instruct-2407-bnb-4bit",
    "unsloth/mistral-7b-v0.3-bnb-4bit",        # Mistral v3 2x faster!
    "unsloth/mistral-7b-instruct-v0.3-bnb-4bit",
    "unsloth/Phi-3.5-mini-instruct",           # Phi-3.5 2x faster!
    "unsloth/Phi-3-medium-4k-instruct",
    "unsloth/gemma-2-9b-bnb-4bit",
    "unsloth/gemma-2-27b-bnb-4bit",            # Gemma 2x faster!
]
  • 幫 LLM 裝上外掛 LoRA
    前面有提到,我們不想要 tune 大模型裡所有的參數,我們想要凍結大模型,只去 train 外掛小模型的參數就好了。
    我們可以用 FastLanguageModelget_peft_model() method,把剛剛宣告的模型以及 LoRA 相關的設定參數傳進去,這樣回傳出來的 model 就會是加上外掛的 model 囉!
model = FastLanguageModel.get_peft_model(
    model,
    r = 16, # Choose any number > 0 ! Suggested 8, 16, 32, 64, 128
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj",],
    lora_alpha = 16,
    lora_dropout = 0, # Supports any, but = 0 is optimized
    bias = "none",    # Supports any, but = "none" is optimized
    # [NEW] "unsloth" uses 30% less VRAM, fits 2x larger batch sizes!
    use_gradient_checkpointing = "unsloth", # True or "unsloth" for very long context
    random_state = 3407,
    use_rslora = False,  # We support rank stabilized LoRA
    loftq_config = None, # And LoftQ
)

我們來逐個解釋一下上面設定到的和 LoRA 有關的參數:
r: 就是 rank,這個參數決定了 LoRA 矩陣的大小。Rank 通常從 8 開始,最多可以設定到 256。雖然較高的 rank 可以存儲更多信息,但會增加 LoRA 的計算和內存成本。我們在這里將其設置為預設值 16。
target_module: LoRA 既然是一個外掛,那我們就可以決定這個外掛要放在模型的哪些 module,包含可以放在 self-attention 層的 Q, K, V, O 矩陣旁邊,或是前饋神經網路的 up/down 投影曾旁邊。加越多地方,要訓練的參數量和內存需求就會越大。這邊我們預設全都加。
lora_alpha: Alpha 直接影響這個外掛 adapter 的貢獻,通常會設定成 r 的兩倍,實驗下來這樣效果會比較好。
use_rslora: 如果打開的話,就會使用 Rank Stabilized LoRA,會讓 LoRA 訓練的時候更穩定,詳細可參考:Rank-Stabilized LoRA: Unlocking the Potential of LoRA Fine-Tuning

  • 準備 Dataset
    這邊我們用 alpaca template 去把我們的 問題, 還有五個選項放進來:
alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
{}

### Input:
{}

### Response:
{}"""

EOS_TOKEN = tokenizer.eos_token # Must add EOS_TOKEN
# %%
def formatting_prompts_func(examples):
    instructions = examples["instruction"]
    inputs       = examples["input"]
    outputs      = examples["output"]
    texts = []
    for instruction, input, output in zip(instructions, inputs, outputs):
        # Must add EOS_TOKEN, otherwise your generation will go on forever!
        text = alpaca_prompt.format(instruction, input, output) + EOS_TOKEN
        texts.append(text)
    return { "text" : texts, }
pass

instruction 的部分放我們自己設計的 prompt:

df['instruction'] = "Here is a multiple-choice question generated from a Wikipedia page on a science-related topic. Each question has five options. After reading the question and the five options, please respond with 'option_1', 'option_2', 'option_3', 'option_4', or 'option_5' to indicate the correct answer. Only provide the answer without any additional explanation. You must select one option, and blank responses are not allowed."

整理成固定格式的 dataset:
trainset 放的是我們自己擴增的那 1000 筆資料,testset 則是 Host 提供的那 200 筆有 ground truth 的資料:

trainset = Dataset.from_pandas(df_train)
trainset =trainset.map(formatting_prompts_func, batched=True)
testset = Dataset.from_pandas(df)
testset =testset.map(formatting_prompts_func, batched=True)
  • 設定訓練參數
    這邊基本上和 HuggingFace 的 Trainer 使用方法是一樣的,我們這邊先設定訓練一個 epoch 就好了。
from trl import SFTTrainer
from transformers import TrainingArguments
from unsloth import is_bfloat16_supported

trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = trainset,
    eval_dataset = testset,  
    dataset_text_field = "text",
    max_seq_length = max_seq_length,
    dataset_num_proc = 2,
    packing = False,  # Can make training 5x faster for short sequences.
    args = TrainingArguments(
        per_device_train_batch_size = 2,
        gradient_accumulation_steps = 4,
        warmup_steps = 5,
        num_train_epochs = 1,  # Set this for 1 full training run.
        # max_steps = 60,
        learning_rate = 2e-4,
        fp16 = not is_bfloat16_supported(),
        bf16 = is_bfloat16_supported(),
        logging_steps = 1,
        eval_steps = 10,  
        evaluation_strategy = "steps",  
        optim = "adamw_8bit",
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 3407,
        output_dir = "outputs",
    ),
)

接下來,開始訓練:

trainer_stats = trainer.train()

我使用單張 RTX 4090 24GB 的顯卡,經過實測,峰值顯存用量為13.292GB.

* 訓練框架: unsloth
* 訓練模型: Meta-Llama-3.1-8B
* 訓練8B模型,最大token長度720,Batch Size 設定為 4,峰值顯存為10.292 GB.
* 1000 筆資料,訓練 1 個 epoch,大概三分鐘跑完。
  • Inference
    訓練完成之後,我們想讓模型在 testset 上 generate 答案,來測看看模型經過一個 epoch 的訓練後,有什麼改變,我們需要先執行這一段:
FastLanguageModel.for_inference(model) 

然後定義一個 generate function:

def generate_outputs(examples):
    # 建立批量的 prompts
    prompts = [
        alpaca_prompt.format(instruction, input_data, "")
        for instruction, input_data in zip(examples["instruction"], examples["input"])
    ]
    
    # 將 prompts 轉換為模型的輸入格式
    inputs = tokenizer(prompts, return_tensors="pt", padding=True, truncation=True).to("cuda")
    
    # 使用模型進行生成
    outputs = model.generate(**inputs, max_new_tokens=5, do_sample=False, use_cache=True)
    
    # 將生成結果解碼
    decoded_outputs = tokenizer.batch_decode(outputs, skip_special_tokens=True)
    
    # 過濾掉 prompts,僅保留生成的 tokens
    generated_texts = []
    for prompt, decoded_output in zip(prompts, decoded_outputs):
        # 刪除 prompt 的部分,只保留其後的生成結果
        generated_text = decoded_output[len(prompt):].strip()
        generated_texts.append(generated_text)
    
    return {"generated_output": generated_texts}

最後,我們就可以在 testset 上生成模型的預測結果:

testset = testset.map(generate_outputs, batched=True, batch_size=8, desc="Generating outputs")

實驗結果

為了瞭解 Meta-Llama-3.1-8B 到底訓練後跟原本有沒有差?

我首先讓 Meta-Llama-3.1-8B 在那兩百筆 testset 上先裸考一遍,看它的準確率如何,接下來再用那 1000 筆資料,從隨機抽取 200, 400, 600, 800 到 1000 筆資料全下,用不同資料量訓練5個模型,觀察他們的 loss curve 以及 accuracy 的變化。

image

我們從 validation loss 可以觀察到,隨著訓練資料量增加,validation loss 也會逐漸下降。都是訓練 1 個 epoch,全部資料下去硬 train 一發的模型,validation loss 來到最低點 0.4986 左右。

那 Accuracy 的表現呢?

Method Accuracy
模型裸考 42.5%
200 筆資料訓練 55%
400 筆資料訓練 55.5%
600 筆資料訓練 58.5%
800 筆資料訓練 60%
1000 筆資料訓練 61%

在 Accuracy 上也表現出隨著訓練資料增加,模型持續增益的現象~

到這邊,過渡章節終於結束了!

明天,我們就會開始進入本賽題的金牌作法解析囉🤓!

我們明天見!


謝謝讀到最後的你,希望你會覺得有趣!
如果喜歡這系列,別忘了按下訂閱,才不會錯過最新更新,也可以按讚⭐️給我鼓勵唷!
如果有任何回饋和建議,歡迎在留言區和我說✨✨


Kaggle - LLM Science Exam 解法分享系列)


上一篇
[Day18]🧐如何選擇適合特定任務的 LLM?深入分析評測 LLM 常用的 Benchmark 與 Leaderboard
下一篇
[Day20]Encoder-only 與 Decoder-only 的路線之爭?淺談 Decoder-only 架構驅動的 RAG Pipeline 建置
系列文
一個Kaggle金牌解法是如何誕生的?跟隨Kaggle NLP競賽高手的討論,探索解題脈絡30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言